package com.ibatis.sqlmap.engine.builder.xml;

import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.util.List;
import java.util.Properties;

import org.w3c.dom.Node;

import com.ibatis.common.resources.Resources;
import com.ibatis.common.xml.Nodelet;
import com.ibatis.common.xml.NodeletParser;
import com.ibatis.common.xml.NodeletUtils;
import com.ibatis.sqlmap.client.SqlMapClient;
import com.ibatis.sqlmap.client.SqlMapException;
import com.ibatis.sqlmap.engine.config.SqlMapConfiguration;
import com.ibatis.sqlmap.engine.datasource.DataSourceFactory;
import com.ibatis.sqlmap.engine.mapping.result.ResultObjectFactory;
import com.ibatis.sqlmap.engine.transaction.TransactionConfig;
import com.ibatis.sqlmap.engine.transaction.TransactionManager;
import com.sky.common.file.FileUtils;
import com.sky.common.string.StringUtils;

public class SqlMapConfigParser {

  protected final NodeletParser parser = new NodeletParser();
  private XmlParserState state = new XmlParserState();

  private boolean usingStreams = false;

  public SqlMapConfigParser() {
    parser.setValidation(true);
    parser.setEntityResolver(new SqlMapClasspathEntityResolver());

    addSqlMapConfigNodelets();
    addGlobalPropNodelets();
    addSettingsNodelets();
    addTypeAliasNodelets();
    addTypeHandlerNodelets();
    addTransactionManagerNodelets();
    addSqlMapNodelets();
    addResultObjectFactoryNodelets();

  }

  public SqlMapClient parse(Reader reader, Properties props) {
    if (props != null) state.setGlobalProps(props);
    return parse(reader);
  }

  public SqlMapClient parse(Reader reader) {
    try {
      usingStreams = false;

      parser.parse(reader);
  	/*=====================================================================*/	  
  		addSqlMapFiles();
  	/*=====================================================================*/      
      return state.getConfig().getClient();
    } catch (Exception e) {
      throw new RuntimeException("Error occurred.  Cause: " + e, e);
    }
  }

  public SqlMapClient parse(InputStream inputStream, Properties props) {
    if (props != null) state.setGlobalProps(props);
    return parse(inputStream);
  }

  public SqlMapClient parse(InputStream inputStream) {
    try {
      usingStreams = true;

      parser.parse(inputStream);
    	/*=====================================================================*/	  
    		addSqlMapFiles();
    	/*=====================================================================*/        
      return state.getConfig().getClient();
    } catch (Exception e) {
      throw new RuntimeException("Error occurred.  Cause: " + e, e);
    }
  }

  private void addSqlMapConfigNodelets() {
    parser.addNodelet("/sqlMapConfig/end()", new Nodelet() {
      public void process(Node node) throws Exception {
        state.getConfig().finalizeSqlMapConfig();
      }
    });
  }

  private void addGlobalPropNodelets() {
    parser.addNodelet("/sqlMapConfig/properties", new Nodelet() {
      public void process(Node node) throws Exception {
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        String resource = attributes.getProperty("resource");
        String url = attributes.getProperty("url");
        state.setGlobalProperties(resource, url);
      }
    });
  }

  private void addSettingsNodelets() {
    parser.addNodelet("/sqlMapConfig/settings", new Nodelet() {
      public void process(Node node) throws Exception {
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        SqlMapConfiguration config = state.getConfig();

        String classInfoCacheEnabledAttr = attributes.getProperty("classInfoCacheEnabled");
        boolean classInfoCacheEnabled = (classInfoCacheEnabledAttr == null || "true".equals(classInfoCacheEnabledAttr));
        config.setClassInfoCacheEnabled(classInfoCacheEnabled);

        String lazyLoadingEnabledAttr = attributes.getProperty("lazyLoadingEnabled");
        boolean lazyLoadingEnabled = (lazyLoadingEnabledAttr == null || "true".equals(lazyLoadingEnabledAttr));
        config.setLazyLoadingEnabled(lazyLoadingEnabled);

        String statementCachingEnabledAttr = attributes.getProperty("statementCachingEnabled");
        boolean statementCachingEnabled = (statementCachingEnabledAttr == null || "true".equals(statementCachingEnabledAttr));
        config.setStatementCachingEnabled(statementCachingEnabled);

        String cacheModelsEnabledAttr = attributes.getProperty("cacheModelsEnabled");
        boolean cacheModelsEnabled = (cacheModelsEnabledAttr == null || "true".equals(cacheModelsEnabledAttr));
        config.setCacheModelsEnabled(cacheModelsEnabled);

        String enhancementEnabledAttr = attributes.getProperty("enhancementEnabled");
        boolean enhancementEnabled = (enhancementEnabledAttr == null || "true".equals(enhancementEnabledAttr));
        config.setEnhancementEnabled(enhancementEnabled);

        String useColumnLabelAttr = attributes.getProperty("useColumnLabel");
        boolean useColumnLabel = (useColumnLabelAttr == null || "true".equals(useColumnLabelAttr));
        config.setUseColumnLabel(useColumnLabel);

        String forceMultipleResultSetSupportAttr = attributes.getProperty("forceMultipleResultSetSupport");
        boolean forceMultipleResultSetSupport = "true".equals(forceMultipleResultSetSupportAttr);
        config.setForceMultipleResultSetSupport(forceMultipleResultSetSupport);

        String defaultTimeoutAttr = attributes.getProperty("defaultStatementTimeout");
        Integer defaultTimeout = defaultTimeoutAttr == null ? null : Integer.valueOf(defaultTimeoutAttr);
        config.setDefaultStatementTimeout(defaultTimeout);

        String useStatementNamespacesAttr = attributes.getProperty("useStatementNamespaces");
        boolean useStatementNamespaces = "true".equals(useStatementNamespacesAttr);
        state.setUseStatementNamespaces(useStatementNamespaces);
      }
    });
  }

  private void addTypeAliasNodelets() {
    parser.addNodelet("/sqlMapConfig/typeAlias", new Nodelet() {
      public void process(Node node) throws Exception {
        Properties prop = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        String alias = prop.getProperty("alias");
        String type = prop.getProperty("type");
        state.getConfig().getTypeHandlerFactory().putTypeAlias(alias, type);
      }
    });
  }

  private void addTypeHandlerNodelets() {
    parser.addNodelet("/sqlMapConfig/typeHandler", new Nodelet() {
      public void process(Node node) throws Exception {
        Properties prop = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        String jdbcType = prop.getProperty("jdbcType");
        String javaType = prop.getProperty("javaType");
        String callback = prop.getProperty("callback");

        javaType = state.getConfig().getTypeHandlerFactory().resolveAlias(javaType);
        callback = state.getConfig().getTypeHandlerFactory().resolveAlias(callback);

        state.getConfig().newTypeHandler(Resources.classForName(javaType), jdbcType, Resources.instantiate(callback));
      }
    });
  }

  private void addTransactionManagerNodelets() {
    parser.addNodelet("/sqlMapConfig/transactionManager/property", new Nodelet() {
      public void process(Node node) throws Exception {
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        String name = attributes.getProperty("name");
        String value = NodeletUtils.parsePropertyTokens(attributes.getProperty("value"), state.getGlobalProps());
        state.getTxProps().setProperty(name, value);
      }
    });
    parser.addNodelet("/sqlMapConfig/transactionManager/end()", new Nodelet() {
      public void process(Node node) throws Exception {
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        String type = attributes.getProperty("type");
        boolean commitRequired = "true".equals(attributes.getProperty("commitRequired"));

        state.getConfig().getErrorContext().setActivity("configuring the transaction manager");
        type = state.getConfig().getTypeHandlerFactory().resolveAlias(type);
        TransactionManager txManager;
        try {
          state.getConfig().getErrorContext().setMoreInfo("Check the transaction manager type or class.");
          TransactionConfig config = (TransactionConfig) Resources.instantiate(type);
          config.setDataSource(state.getDataSource());
          state.getConfig().getErrorContext().setMoreInfo("Check the transactio nmanager properties or configuration.");
          config.setProperties(state.getTxProps());
          config.setForceCommit(commitRequired);
          config.setDataSource(state.getDataSource());
          state.getConfig().getErrorContext().setMoreInfo(null);
          txManager = new TransactionManager(config);
        } catch (Exception e) {
          if (e instanceof SqlMapException) {
            throw (SqlMapException) e;
          } else {
            throw new SqlMapException("Error initializing TransactionManager.  Could not instantiate TransactionConfig.  Cause: " + e, e);
          }
        }
        state.getConfig().setTransactionManager(txManager);
      }
    });
    parser.addNodelet("/sqlMapConfig/transactionManager/dataSource/property", new Nodelet() {
      public void process(Node node) throws Exception {
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        String name = attributes.getProperty("name");
        String value = NodeletUtils.parsePropertyTokens(attributes.getProperty("value"), state.getGlobalProps());
        state.getDsProps().setProperty(name, value);
      }
    });
    parser.addNodelet("/sqlMapConfig/transactionManager/dataSource/end()", new Nodelet() {
      public void process(Node node) throws Exception {
        state.getConfig().getErrorContext().setActivity("configuring the data source");

        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());

        String type = attributes.getProperty("type");
        Properties props = state.getDsProps();

        type = state.getConfig().getTypeHandlerFactory().resolveAlias(type);
        try {
          state.getConfig().getErrorContext().setMoreInfo("Check the data source type or class.");
          DataSourceFactory dsFactory = (DataSourceFactory) Resources.instantiate(type);
          state.getConfig().getErrorContext().setMoreInfo("Check the data source properties or configuration.");
          dsFactory.initialize(props);
          state.setDataSource(dsFactory.getDataSource());
          state.getConfig().getErrorContext().setMoreInfo(null);
        } catch (Exception e) {
          if (e instanceof SqlMapException) {
            throw (SqlMapException) e;
          } else {
            throw new SqlMapException("Error initializing DataSource.  Could not instantiate DataSourceFactory.  Cause: " + e, e);
          }
        }
      }
    });
  }


  protected void addSqlMapNodelets() {
    parser.addNodelet("/sqlMapConfig/sqlMap", new Nodelet() {
      public void process(Node node) throws Exception {
        state.getConfig().getErrorContext().setActivity("loading the SQL Map resource");

        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());

        String resource = attributes.getProperty("resource");
        String url = attributes.getProperty("url");

        if (usingStreams) {
          InputStream inputStream = null;
          if (resource != null) {
            state.getConfig().getErrorContext().setResource(resource);
            inputStream = Resources.getResourceAsStream(resource);
          } else if (url != null) {
            state.getConfig().getErrorContext().setResource(url);
            inputStream = Resources.getUrlAsStream(url);
          } else {
            throw new SqlMapException("The <sqlMap> element requires either a resource or a url attribute.");
          }

          new SqlMapParser(state).parse(inputStream);
        } else {
          Reader reader = null;
          if (resource != null) {
            state.getConfig().getErrorContext().setResource(resource);
            reader = Resources.getResourceAsReader(resource);
          } else if (url != null) {
            state.getConfig().getErrorContext().setResource(url);
            reader = Resources.getUrlAsReader(url);
          } else {
            throw new SqlMapException("The <sqlMap> element requires either a resource or a url attribute.");
          }

          new SqlMapParser(state).parse(reader);
        }
      }
    });
  }

/*=====================================================================*/
	private static final String FILE_SUFFIX = ".ibatis.xml";
	//自动加载
	private void addSqlMapFiles() throws Exception {
		String dialect = getDialect();
		state.getConfig().getErrorContext().setActivity("loading the SQL Map file");
		List<File> files = FileUtils.getAllFile(new File(FileUtils.getClassPath()), FILE_SUFFIX);
		String dialectName;//带别名的文件名称
		File dialectFile;//带别名的文件对象
		for (File file : files) {
			dialectName=StringUtils.replaceAll(file.getName(), FILE_SUFFIX, "");//去掉统一后缀
			if(dialectName.lastIndexOf(".")!=-1){//最后面有点号
				dialectName=dialectName.substring(0, dialectName.lastIndexOf(".")+1);//去掉原别名
				dialectName=dialectName+dialect+FILE_SUFFIX;//增加当前数据库别名
				dialectFile=new File(file.getParentFile(),dialectName);
				if(!dialectName.equals(file.getName())//当前文件不是别名文件
						&&dialectFile.exists()){//别名文件存在
					System.out.println("SQLMAP文件【" + file.getName() + "】存在别名文件【"+dialectName+"】，略过当前文件...");
					continue;
				}
			}
			try {
				if (usingStreams) {
					InputStream inputStream = null;
					String resource = StringUtils.replaceAll(file.getPath(), FileUtils.getClassPath(), "");
					state.getConfig().getErrorContext().setResource(resource);
					inputStream = Resources.getResourceAsStream(resource);
					new SqlMapParser(state).parse(inputStream);
				} else {
					Reader reader = null;
					String resource = StringUtils.replaceAll(file.getPath(), FileUtils.getClassPath(), "");
					state.getConfig().getErrorContext().setResource(resource);
					reader = Resources.getResourceAsReader(resource);
					new SqlMapParser(state).parse(reader);
				}
			} catch (Exception e) {
				throw new Exception("解析SQLMAP文件【" + file.getPath() + "】错误：" + e.getMessage(), e);
			}
		}
	}
	//获取数据库产品名称
	private String getDialect() throws Exception {
		Connection con = state.getConfig().getClient().getDataSource().getConnection();
		DatabaseMetaData metaData = con.getMetaData();
		String productName = metaData.getDatabaseProductName();
		String dialect=productName;
		con.close();
		if("Microsoft SQL Server".equalsIgnoreCase(productName)){
			dialect="SQLServer";
		}else if("Oracle".equalsIgnoreCase(productName)){
			dialect="Oracle";
		}else{
			throw new Exception("未处理的数据库类型【"+productName+"】别名，请与系统管理员联系！");
		}
		System.out.println("数据库类型【"+productName+"】的数据库别名【"+dialect+"】...");
		return dialect;
	}
/*=====================================================================*/
  private void addResultObjectFactoryNodelets() {
    parser.addNodelet("/sqlMapConfig/resultObjectFactory", new Nodelet() {
      public void process(Node node) throws Exception {
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        String type = attributes.getProperty("type");

        state.getConfig().getErrorContext().setActivity("configuring the Result Object Factory");
        ResultObjectFactory rof;
        try {
          rof = (ResultObjectFactory) Resources.instantiate(type);
          state.getConfig().setResultObjectFactory(rof);
        } catch (Exception e) {
          throw new SqlMapException("Error instantiating resultObjectFactory: " + type, e);
        }

      }
    });
    parser.addNodelet("/sqlMapConfig/resultObjectFactory/property", new Nodelet() {
      public void process(Node node) throws Exception {
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        String name = attributes.getProperty("name");
        String value = NodeletUtils.parsePropertyTokens(attributes.getProperty("value"), state.getGlobalProps());
        state.getConfig().getDelegate().getResultObjectFactory().setProperty(name, value);
      }
    });
  }

}
